<?php
#
# dmBridge: a data access framework for CONTENTdm(R)
#
# Copyright © 2009, 2010, 2011 Board of Regents of the Nevada System of Higher
# Education, on behalf of the University of Nevada, Las Vegas
#

/**
 * <p>A class consisting of helper methods to assist in creating web page
 * templates. These methods are specific to results view and are neither
 * guaranteed nor expected to work in any other view.</p>
 *
 * <p>This class was called ResultsDraw in dmBridge 1.x.</p>
 *
 * @author Alex Dolski <alex.dolski@unlv.edu>
 * @license http://www.opensource.org/licenses/mit-license.php
 */
class DMResultsTemplateHelper extends DMGenericTemplateHelper
implements DMTemplateHelper {

	/**
	 * Returns an HTML unordered list of facets. The outer &lt;ul&gt; element
	 * has class <strong>dmFacetList</strong>; the inner &lt;ul&gt; elements
	 * have class <strong>dmFacet</strong>; the facet term &lt;li&gt; elements
	 * have class <strong>dmFacetTerm</strong>; and the facet term count
	 * &lt;span&gt; elements have class <strong>dmFacetTermCount</strong>. If no
	 * facets are available, returns null.
	 *
	 * @param int term_cutoff Omit any more than this number of terms within a
	 * facet; 0 = disable
	 * @return string
	 */
	public function getHtmlFacetsAsUL($term_cutoff = 0) {
		$query = $this->getView()->getQuery();
		if (!$query || count($query->getFacetTerms()) < 1) {
			return "<!-- No facets -->";
		}

		$dxml = new DMDOMDocument("1.0", "utf-8");
		$dxml->loadXML("<ul/>");
		$dxml->documentElement->setAttribute("class", "dmFacetList");

		// preprocess the facet terms structure into something easier to get
		// into a DOM tree
		$facets = array();
		foreach ($query->getFacetTerms() as $term) {
			$facets[$term->getField()->getName()][] = array(
				'value' => $term->getField()->getValue(),
				'count' => $term->getCount(),
				'nick' => $term->getField()->getNick(),
				'uri' => (string) $term->getURI()
			);
		}

		foreach ($facets as $name => $terms) {
			$fnode = $dxml->createElement("ul");
			$fnode->setAttribute("class", "dmFacet");

			for ($i = 0; $i < count($terms); $i++) {
				if ($term_cutoff > 0 && $i > $term_cutoff) {
					break;
				}
				$uri = DMHTTPRequest::getCurrent()->getURI();
				// skip any facets that are already part of the search query
				foreach ($query->getPredicates() as $st) {
					if (DMString::paranoid($terms[$i]['value'])
							== DMString::paranoid($st->getString())) {
						continue(2);
					}
				}

				// assemble target href
				for ($j = 1; $j < 5; $j++) {
					if (!$uri->getQueryValue("CISOBOX" . $j)) {
						if ($j <= count($terms) && $terms[$i]) {
							$uri->addQueryValue("CISOBOX" . $j,
									DMString::paranoid(
										$terms[$i]['value'],
										array(' ', '-')));
							$uri->addQueryValue("CISOFIELD" . $j,
									$terms[$i]['nick']);
							$uri->addQueryValue('CISOOP' . $j, "all");
							break;
						}
					}
				}

				// assemble html elements
				$term = $dxml->createElement("a",
						DMString::websafe($terms[$i]['value']));
				$term->setAttribute("href", $uri);
				$count = $dxml->createElement(
					"span", sprintf("(%d)", $terms[$i]['count']));
				$count->setAttribute("class", "dmFacetTermCount");
				$li = $dxml->createElement("li");
				$li->setAttribute("class", "dmFacetTerm");
				$li->appendChild($term);
				$li->appendChild($dxml->createTextNode(" "));
				$li->appendChild($count);
				$fnode->appendChild($li);
			}

			if ($fnode->childNodes->length) {
				$flnode = $dxml->createElement('li', DMString::websafe($name));
				$flnode->setAttribute('class', 'dmFacetTitle');
				$dxml->documentElement->appendChild($flnode);
				$dxml->documentElement->appendChild($fnode);
			}

		}
		if ($dxml->documentElement->childNodes->length) {
			return $dxml->saveHTML($dxml->documentElement);
		}
		return "";
	}

	/**
	 * Returns a search suggestion (e.g. "Did you mean...") hyperlinked to a
	 * search for that suggestion.
	 *
	 * @return string|null
	 * @since 0.5
	 */
	public function getHtmlLinkedSuggestion() {
		$query = $this->getView()->getQuery();
		if ($query->getSuggestion()) {
			$term = $query->getSuggestion();
			$qt = new DMQueryPredicate();
			$qt->setString($query->getSuggestion());
			$qt->setField(new DMDCElement("all"));
			$qt->setMode("all");
			$query->setPredicates(array($qt));
			return $this->getHtmlTermLinkedToSearch($term, $query);
		}
		return null;
	}

	/**
	 * @param int max_page_links The maximum number of <strong>links</strong>
	 * (not numbers) allowed
	 * @param string page_list_separator A string that should separate the page
	 * links
	 * @param string current_page_element The name of the HTML element that
	 * should wrap the current page number (without angle brackets)
	 * @param string first_symbol
	 * @param string prev_symbol
	 * @param string next_symbol
	 * @param string last_symbol
	 * @return string
	 * @since 0.1
	 */
	public function getHtmlPageLinks(
			$max_page_links = 10, $page_list_separator = ' ',
			$current_page_element = 'strong',
			$first_symbol = '<<', $prev_symbol = '<',
			$next_symbol = '>', $last_symbol = '>>') {

		// we are going to build the URI by cloning our current DMObjectQuery,
		// modifying it appropriately, and inserting the return value of its
		// getURI() method within the page number <a> tags.
		$query = $this->getView()->getQuery();
		$query_copy = clone $query;
		
		$current_uri = DMHTTPRequest::getCurrent()->getURI();

		$ts = $this->getView()->getTemplate()->getTemplateSet();

		$current_page_open_tag = sprintf('<%s>',
			trim($current_page_element, '<>'));
		$current_page_close_tag = sprintf('</%s>',
			trim($current_page_element, '<>'));

		$num_pages = $query->getNumPages();

		if ($num_pages < 1
				|| $this->getView() instanceof DMFavoritesView) {
			return $current_page_open_tag . "1" . $current_page_close_tag;
		}

		$page_links = array();
		// "first" link, before all other links
		if ($query->getPage() > 1) {
			$query_copy->setPage(1);
			$page_links[] = sprintf('<a href="%s">%s</a>',
				$query_copy->getURI(DMBridgeComponent::TemplateEngine, "html",
					$ts, $current_uri),
					$first_symbol);
		} else {
			$page_links[0] = $first_symbol;
		}

		// "previous" link, after "first" link, before page # links
		if ($query->getPage() > 1) {
			$query_copy->setPage($query->getPage() - 1);
			$page_links[] = sprintf('<a href="%s">%s</a>',
				$query_copy->getURI(DMBridgeComponent::TemplateEngine, "html",
					$ts, $current_uri),
					$prev_symbol);
		} else {
			$page_links[] = $prev_symbol;
		}

		// enforce $max_page_links
		$startpage = 1;
		$stoppage = $query->getNumPages();
		if ($query->getNumPages() > $max_page_links) {
			$startpage = $query->getPage() - floor($max_page_links / 2);
			if ($startpage < 1) {
				$startpage = 1;
			}
			$stoppage = $query->getPage() + floor($max_page_links / 2);
			if ($stoppage - $startpage < $max_page_links) {
				$stoppage = $startpage + $max_page_links;
			}
			if ($stoppage > $query->getNumPages()) {
				$startpage = $query->getNumPages()
						- $max_page_links;
				$stoppage = $query->getNumPages();
			}
		}

		// build page number links
		for ($page = $startpage; $page <= $stoppage; $page++) {
			if ($page == $query->getPage()) {
				$page_links[] = $page;
			} else if ($page == 1 || $page <> $query->getPage()) {
				$query_copy->setPage($page);
				$page_links[] = sprintf('<a href="%s">%s</a>',
					$query_copy->getURI(DMBridgeComponent::TemplateEngine,
						"html", $ts, $current_uri),
						$page);
			} else {
				$page_links[] = $current_page_open_tag
					. $page . $current_page_close_tag;
			}
		}

		// "next" link, after page # links, before "last" link
		if ($query->getPage() < $query->getNumPages()) {
			$query_copy->setPage($query->getPage() + 1);
			$page_links[] = sprintf('<a href="%s">%s</a>',
				$query_copy->getURI(DMBridgeComponent::TemplateEngine, "html",
					$ts, $current_uri),
					$next_symbol);
		} else {
			$page_links[] = $next_symbol;
		}

		// "last" link, after all other links
		if ($query->getPage() < $query->getNumPages()) {
			$query_copy->setPage($query->getNumPages());
			$page_links[] = sprintf('<a href="%s">%s</a>',
				$query_copy->getURI(DMBridgeComponent::TemplateEngine, "html", $ts,
					$current_uri),
					$last_symbol);
		} else {
			$page_links[] = $last_symbol;
		}
		return implode($page_list_separator, $page_links);
	}

	/**
	 * Returns an HTML table element corresponding to the requested results
	 * view, specified first by the "view" query string parameter, and last by
	 * the $view parameter. If neither are present, defaults to grid view.
	 *
	 * @param string view One of "grid," "list," or "tile"
	 * @return string
	 * @see getHtmlViewLinks()
	 */
	public function getHtmlResults($view = "grid") {
		$uri = DMHTTPRequest::getCurrent()->getURI();

		$valid_views = array('grid', 'list', 'tile');
		if (in_array($uri->getQueryValue("view"), $valid_views)) {
			$view = $uri->getQueryValue("view");
		}

		switch ($view) {
		case 'tile':
			return $this->getHtmlResultsAsTiles();
			break;
		case 'list':
			return $this->getHtmlResultsAsList();
			break;
		default:
			return $this->getHtmlResultsAsGrid();
			break;
		}
	}

	/**
	 * @since 0.1
	 */
	public function getHtmlResultsAsGrid() {
		$query = $this->getView()->getQuery();
		$objects = $query->getSearchResults();
		$view = $this->getView();
		$fields = $view->getFields();
		$count = count($objects);

		if ($count < 1) {
			return null;
		}

		$dxml = $this->getResultsForm();

		$table = $dxml->documentElement->childNodes->item(0);
		$table->setAttribute("cellspacing", 0);
		$table->setAttribute("cellpadding", 0);
		$table->setAttribute("border", 0);
		$table->setAttribute("class", "dmResults dmGridResults");

		// header row
		$tr = $dxml->createElement("tr");
		$th = $dxml->createElement("th", "&nbsp;");
		$tr->appendChild($th);
		foreach ($fields as $f) {
			// IE allegedly needs the nbsps
			$th = $dxml->createElement("th",
				($f->getName()) ? DMString::websafe($f->getName()) : "&nbsp;");
			$tr->appendChild($th);
		}
		$table->appendChild($tr);

		// rows
		for ($i = 0; $i < $count; $i++) {
			$tr = $dxml->createElement("tr");
			if (!($i & 1)) {
				$tr->setAttribute("class", "odd");
			}
			$td = $dxml->createElement("td");
			$input = $dxml->createElement("input");
			$input->setAttribute("type", "checkbox");
			$input->setAttribute("name", "data[]");
			$input->setAttribute("value",
				DMString::websafe($objects[$i]->getCollection()->getAlias()
				. DMAbstractForm::ALIAS_PTR_SEPARATOR . $objects[$i]->getPtr()));
			if ($objects[$i]->isFavorite()
					&& !$this instanceof DMFavoritesTemplateHelper) {
				$input->setAttribute("disabled", "disabled");
			}
			$td->appendChild($input);
			$tr->appendChild($td);

			$url = $objects[$i]->getURI();

			// columns
			foreach ($fields as $f) {
				if ($f->getNick() == "index") { // tack on index
					$td = $dxml->createElement("td",
						($query->getPage() - 1) * $query->getNumResultsPerPage()
							+ $i + 1);
					$tr->appendChild($td);
				} else if ($f->getNick() == "thumb") { // tack on thumbnail
					$td = $dxml->createElement("td");
					$a = $dxml->createElement("a");
					$a->setAttribute("href", $url);

					$img = $dxml->createElement("img");
					$img->setAttribute("src", $objects[$i]->getThumbURL());
					$img->setAttribute("class", "dmThumbnail");
					$img->setAttribute("alt", "Object thumbnail");

					$a->appendChild($img);
					$td->appendChild($a);

					$div = $dxml->createElement("div");
					$div->setAttribute("class", "dmResultIcons");
					$td->appendChild($div);
					$this->appendIconSpanElementsToParent($objects[$i], $dxml, $div);

					$tr->appendChild($td);
				} else {
					$td = $dxml->createElement("td");
					$td->setAttribute("class",
						sprintf("dmGrid%sField", ucfirst($f->getNick())));
					$tmp = $objects[$i]->getMetadata($f->getNick());
					if ($tmp) {
						$value = rtrim($tmp->getValue(), "/");
						// truncate field, if maxwords specified
						if ($f->getMaxWords() > 0) {
							$value = DMString::truncate($value, $f->getMaxWords());
						}
						if ($f->getNick() == "title") {
							$a = $dxml->createElement("a", DMString::websafe($value));
							$a->setAttribute("href", $url);
							$td->appendChild($a);
						} else {
							$td->nodeValue = DMString::websafe($value);
						}
					}
				}
				$tr->appendChild($td);
			} // foreach
			$table->appendChild($tr);
		} // for

		$dxml->formatOutput = true;
		return $dxml->saveHTML($dxml->documentElement);
	}

	/**
	 * @since 0.1
	 */
	public function getHtmlResultsAsList() {
		$view = $this->getView();
		$query = $view->getQuery();
		$objects = $query->getSearchResults();
		$fields = $view->getFields();
		$count = count($objects);

		if ($count < 1) {
			return null;
		}

		$dxml = $this->getResultsForm();

		$table = $dxml->documentElement->childNodes->item(0);
		$table->setAttribute("cellspacing", 0);
		$table->setAttribute("cellpadding", 0);
		$table->setAttribute("border", 0);
		$table->setAttribute("class", "dmResults dmListResults");

		// object rows
		for ($i = 0; $i < $count; $i++) {
			$tr = $dxml->createElement("tr");
			if (!($i & 1)) {
				$tr->setAttribute("class", "odd");
			}
			$td = $dxml->createElement("td");
			$input = $dxml->createElement("input");
			$input->setAttribute("type", "checkbox");
			$input->setAttribute("name", "data[]");
			$input->setAttribute("value",
				DMString::websafe($objects[$i]->getCollection()->getAlias()
						. DMAbstractForm::ALIAS_PTR_SEPARATOR
						. $objects[$i]->getPtr()));
			if ($objects[$i]->isFavorite()
					&& !$this instanceof DMFavoritesTemplateHelper) {
				$input->setAttribute("disabled", "disabled");
			}
			$td->appendChild($input);
			$tr->appendChild($td);

			$url = $objects[$i]->getURI();

			// tack on index & thumbnail
			foreach ($fields as $f) {
				if ($f->getNick() == "index") {
					$td = $dxml->createElement("td",
						($query->getPage() - 1) * $query->getNumResultsPerPage()
							+ $i + 1);
					$tr->appendChild($td);
				} else if ($f->getNick() == "thumb") {
					$td = $dxml->createElement("td");
					$a = $dxml->createElement("a");
					$a->setAttribute("href", $url);
					$img = $dxml->createElement("img");
					$img->setAttribute("src", $objects[$i]->getThumbURL());
					$img->setAttribute("class", "dmThumbnail");
					$img->setAttribute("alt", "Object thumbnail");
					$a->appendChild($img);
					$td->appendChild($a);
					$tr->appendChild($td);
				}
			}

			$td = $dxml->createElement("td");
			$mtable = $dxml->createElement("table");
			$mtable->setAttribute("class", "dmListResultMetadata");
			$mtable->setAttribute("cellpadding", 0);
			$mtable->setAttribute("cellspacing", 2);
			$mtable->setAttribute("border", 0);

			// append icon spans
			$mtr = $dxml->createElement("tr");
			$mtd = $dxml->createElement("td");
			$this->appendIconSpanElementsToParent($objects[$i], $dxml, $mtd);
			$mtr->appendChild($mtd);
			$mtable->appendChild($mtr);

			// fill in fields
			$j = 0;
			foreach ($fields as $f) {
				if ($f->getNick() == "index" || $f->getNick() == "thumb") {
					continue;
				}

				$mtr = $dxml->createElement("tr");
				if (!($j & 1)) {
					$mtr->setAttribute("class", "odd");
				}

				$mth = $dxml->createElement("th",
					DMString::websafe($f->getName()));
				$mth->setAttribute("class",
					sprintf("dmListFieldName dmList%sFieldName",
						DMString::websafe(ucfirst($f->getNick()))));

				$mtd = $dxml->createElement("td");
				$mtd->setAttribute("class",
					sprintf("dmListFieldValue dmList%sFieldValue",
						DMString::websafe(ucfirst($f->getNick()))));

				$tmp = $objects[$i]->getMetadata($f->getNick());

				$value = ($tmp instanceof DMDCElement) ? $tmp->getValue() : "";
				// truncate field, if maxwords specified
				if ($f->getMaxWords() > 0) {
					$value = DMString::truncate($value, $f->getMaxWords());
				}
				if ($f->getNick() == "title") {
					$a = $dxml->createElement("a", DMString::websafe($value));
					$a->setAttribute("href", $url);
					$mtd->appendChild($a);
				} else {
					$mtd->nodeValue = DMString::websafe($value);
				}
				$mtr->appendChild($mth);
				$mtr->appendChild($mtd);
				$mtable->appendChild($mtr);
				$j++;
			} // foreach
			$td->appendChild($mtable);
			$tr->appendChild($td);
			$table->appendChild($tr);
		} // for

		$dxml->formatOutput = true;
		return $dxml->saveHTML($dxml->documentElement);
	}

	/**
	 * @since 0.1
	 */
	public function getHtmlResultsAsTiles() {
		$view = $this->getView();
		$query = $view->getQuery();
		$objects = $query->getSearchResults();
		$count = count($objects);
		$num_columns = $view->getNumTileViewColumns()
				? $view->getNumTileViewColumns() : 3;

		if ($count < 1) {
			return null;
		}

		$dxml = $this->getResultsForm();

		$table = $dxml->documentElement->childNodes->item(0);
		$table->setAttribute("cellspacing", 0);
		$table->setAttribute("cellpadding", 0);
		$table->setAttribute("border", 0);
		$table->setAttribute("class", "dmResults dmTileResults");

		$tr = $dxml->createElement("tr");
		$j = 0;
		for ($i = 0; $i < $count; $i++) {
			if ($i % $num_columns == 0 && $i != 0) {
				$j++;
				$tr = $dxml->createElement("tr");
			}
			if (!($j & 1)) {
				$tr->setAttribute("class", "odd");
			}

			$td = $dxml->createElement("td");
			$url = $objects[$i]->getURI();

			// thumbnail
			$a = $dxml->createElement("a");
			$a->setAttribute("href", $url);
			$img = $dxml->createElement("img");
			$img->setAttribute("src", $objects[$i]->getThumbURL());
			$img->setAttribute("class", "dmThumbnail");
			$img->setAttribute("alt", "Object thumbnail");
			$a->appendChild($img);
			$td->appendChild($a);
			$br = $dxml->createElement("br");
			$td->appendChild($br);

			// span icons
			$div = $dxml->createElement("div");
			$div->setAttribute("class", "dmResultIcons");
			$td->appendChild($div);
			$this->appendIconSpanElementsToParent($objects[$i], $dxml, $div);

			// checkbox
			$input = $dxml->createElement("input");
			$input->setAttribute("type", "checkbox");
			$input->setAttribute("name", "data[]");
			$input->setAttribute("value",
				DMString::websafe($objects[$i]->getCollection()->getAlias()
					. DMAbstractForm::ALIAS_PTR_SEPARATOR . $objects[$i]->getPtr()));
			if ($objects[$i]->isFavorite()
					&& !$this instanceof DMFavoritesTemplateHelper) {
				$input->setAttribute("disabled", "disabled");
			}
			$td->appendChild($input);

			// index
			$idx = $dxml->createElement("span",
				(($query->getPage() - 1)
					* $query->getNumResultsPerPage() + $i + 1) . ". ");
			$idx->setAttribute("class", "dmIndex");
			$td->appendChild($idx);

			// title
			$field = $objects[$i]->getMetadata("title");
			$value = $field->getValue();
			// truncate field, if maxwords specified
			if ($field->getMaxWords() > 0) {
				$value = DMString::truncate($value, $field->getMaxWords());
			}
			$a = $dxml->createElement("a", DMString::websafe($value));
			$a->setAttribute("href", $url);
			$td->appendChild($a);

			$tr->appendChild($td);
			if ($i % $num_columns == 0) {
				$table->appendChild($tr);
			}
		} // for

		$dxml->formatOutput = true;
		return $dxml->saveHTML($dxml->documentElement);
	}

	private function appendIconSpanElementsToParent(DMObject $obj,
			DOMDocument $dxml, DOMNode $parent) {
		// metadata icon
		$a = $dxml->createElement("a");
		$a->setAttribute("href", DMString::xmlentities(
				$obj->getURI(DMBridgeComponent::TemplateEngine, null, false)));
		$span = $dxml->createElement("span");
		$span->setAttribute("class", "dmResultIcon dmResultViewMetadata");
		$span->setAttribute("title", "View object metadata");
		$a->appendChild($span);
		$parent->appendChild($a);

		// URL-type object icon
		$span = $dxml->createElement("span");
		if ($obj->isURLType()) {
			$a = $dxml->createElement("a");
			$a->setAttribute("href", DMString::xmlentities($obj->getFileURL()));
			$span->setAttribute("class", "dmResultIsURL");
			$span->setAttribute("title", "This is a pointer to another object");
			$a->appendChild($span);
			$parent->appendChild($a);
		} else {
			$span->setAttribute("class", "dmResultIsNotURL");
			$span->setAttribute("title", "This object does not redirect anywhere");
			$parent->appendChild($span);
		}

		// favorite icon
		$span = $dxml->createElement("span");
		if ($obj->isFavorite()) {
			$span->setAttribute("class", "dmResultIsFavorite");
			$span->setAttribute("title", "This object is in your favorites");
		} else {
			$span->setAttribute("class", "dmResultIsNotFavorite");
			$span->setAttribute("title", "This object is not in your favorites");
		}
		$parent->appendChild($span);

		// comments icon
		$span = $dxml->createElement("span");
		if ($obj->hasComments()) {
			$span->setAttribute("class", "dmResultHasComments");
			$span->setAttribute("title", "This object has comments");
		} else {
			$span->setAttribute("class", "dmResultDoesNotHaveComments");
			$span->setAttribute("title", "This object has no comments");
		}
		$parent->appendChild($span);

		// rating icon
		$span = $dxml->createElement("span");
		if ($obj->getRating()) {
			$span->setAttribute("class", "dmResultHasBeenRated");
			$span->setAttribute("title", "This object has been rated");
		} else {
			$span->setAttribute("class", "dmResultHasNotBeenRated");
			$span->setAttribute("title", "This object has not yet been rated");
		}
		$parent->appendChild($span);
	}

	/**
	 * @return Comma-separated, HTML-escaped list of search terms
	 * @since 0.1
	 */
	public function getHtmlSearchTerms() {
		$mode_labels = array(
			'all' => 'all of',
			'exact' => 'exactly',
			'any' => 'any of',
			'none' => 'none of'
		);
		$formatted = array();
		foreach ($this->getView()->getQuery()->getPredicates() as $t) {
			if ($t->getField()->getNick() == 'date') {
				$tmp = explode('-', $t->getString());
				if (count($tmp) > 1) { // "before", "after", or "from" search
					if ($tmp[0] == '00000000' && $tmp[1] == '99999999') {
						$formatted[] = 'any date';
					} else if ($tmp[0] == '00000000') {
						$dt = new DMDateTime($tmp[1]);
						$formatted[] = 'before ' . $dt->format('Y-m-d');
					} else if ($tmp[1] == '99999999') {
						$dt = new DMDateTime($tmp[0]);
						$formatted[] = 'after ' . $dt->format('Y-m-d');
					} else {
						$from = new DMDateTime($tmp[0]);
						$to = new DMDateTime($tmp[1]);
						$formatted[] = $from->format('Y-m-d') . ' to '
							. $to->format('Y-m-d');
					}
				} else { // "on" search
					$dt = new DMDateTime($tmp[0]);
					$formatted[] = 'exactly '. $dt->format('Y-m-d');
				}
			} else $formatted[] = $mode_labels[$t->getMode()]
				. ' "' . $t->getString() . '"';
		}
		return DMString::websafe(implode(', ', $formatted));
	}

	/**
	 * Returns null if fewer than two fields are sortable.
	 *
	 * @param string current_sort_tag HTML tag without the angle brackets
	 * @return string
	 * @since 0.4
	 */
	public function getHtmlSortLinksAsUL($current_sort_tag = 'strong') {
		// if we are on the favorites page, nothing is sortable because the
		// favorites are being pulled from cookies, not a search engine, and
		// we are too lazy to write in favorites sorting
		if ($this instanceof DMFavoritesTemplateHelper) {
			return;
		}

		$query = array();
		$dxml = new DMDOMDocument('1.0', 'utf-8');
		$dxml->loadXML('<ul/>');
		$dxml->documentElement->setAttribute('class', 'dmSortLinks');

		foreach ($this->getView()->getCollection()->getFields() as $f) {
			if (!$f->isSortable()) {
				continue;
			}
			$query['sort'] = $f->getNick();
			$li = $dxml->createElement('li');
			$sort = DMHTTPRequest::getCurrent()->getURI()->getQueryValue("sort");

			if ($f->isDefaultSort() && strlen($sort) < 1) {
				$a = $dxml->createElement('a');
				$inner = $dxml->createElement(
					$current_sort_tag,
					DMString::websafe($f->getName()));
				$a->setAttribute('href', DMInternalURI::getURIWithParams(null, $query));
				$a->appendChild($inner);
				$li->appendChild($a);
			} else if ($f->getNick() == $sort) {
				$value = $dxml->createElement(
					$current_sort_tag,
					DMString::websafe($f->getName()));
				$li->appendChild($value);
			} else if (strlen($sort) < 1) {
				$a = $dxml->createElement('a', DMString::xmlentities($f->getName()));
				$a->setAttribute('href', DMInternalURI::getURIWithParams(null, $query));
				$li->appendChild($a);
			} else {
				$a = $dxml->createElement('a', DMString::xmlentities($f->getName()));
				$a->setAttribute('href', DMInternalURI::getURIWithParams(null, $query));
				$li->appendChild($a);
			}
			$dxml->documentElement->appendChild($li);
		}
		if ($dxml->documentElement->childNodes->length < 2) {
			return "<!-- Not enough sortable fields -->";
		}
		$dxml->formatOutput = true;
		return $dxml->saveHTML($dxml->documentElement);
	}

	/**
	 * Renders the view toggle links that allow selection of different results
	 * view styles. Built-in views include grid, list, and tile. It is possible
	 * to override this by overriding <code>getHtmlResults()</code> in a
	 * custom template helper.
	 *
	 * @param string default_view
	 * @param string separator
	 * @param array views Array of arrays with "name" and "url" keys
	 * corresponding to the name of the view and the URL query string value
	 * ("?view=X") respectively (see $views as defined in the method)
	 * @return string
	 * @since 0.1
	 */
	public function getHtmlViewLinks($default_view = "grid",
			$separator = " | ",
			array $views = array()) {
		if (count($views) < 1) {
			$views = array(
				array(
					'name' => 'Grid',
					'url' => 'grid'
				),
				array(
					'name' => 'List',
					'url' => 'list'
				),
				array(
					'name' => 'Tile',
					'url' => 'tile'
				)
			);
		}

		$req = DMHTTPRequest::getCurrent();
		$view_links = array();

		$current_view = $req->getURI()->getQueryValue("view");
		$current_view = $current_view ? $current_view : $default_view;

		foreach ($views as $view) {
			$uri = clone $req->getURI();
			$uri->unsetQueryKey("view");
			// assign CSS classes to the views
			$view['css'] = sprintf("dm%sViewLink",
				ucwords(str_replace(" ", "", $view['name'])));

			if ($view['url'] == $current_view) {
				$view_links[] = sprintf('<span class="%s">%s</span>',
					$view['css'], $view['name']);
			} else {
				if ($default_view && $view['url'] != $default_view) {
					$uri->setQueryValue("view", $view['url']);
				}
				$view_links[] = sprintf('<a href="%s" class="%s">%s</a>',
					$uri, $view['css'], $view['name']);
			}
		}
		return implode($separator, $view_links);
	}

	/**
	 * @return DOMDocument
	 */
	protected function getResultsForm() {
		$alias = $this->getView()->getCollection()->getAlias();
		$alias = ($alias == "/dmdefault") ? "" : $alias;

		$dxml = new DMDOMDocument('1.0', 'utf-8');
		$dxml->loadXML('<form/>');
		$dxml->documentElement->setAttribute('class', 'dmResults');
		$table = $dxml->createElement('table');
		$dxml->documentElement->appendChild($table);

		$params = $alias ? "objects" . $alias . "/favorites" : "objects/favorites";
		$dxml->documentElement->setAttribute("action",
				DMInternalURI::getURIWithParams($params));
		$dxml->documentElement->setAttribute("method", "post");

		$input_div = $dxml->createElement('div');

		$input = $dxml->createElement('input');
		$input->setAttribute('type', 'hidden');
		$input->setAttribute('name', 'params');
		$input->setAttribute('value',
				sprintf("objects%s/favorites", rtrim($alias, "/")));
		$input_div->appendChild($input);

		$input = $dxml->createElement('input');
		$input->setAttribute('type', 'submit');
		$input->setAttribute('class', 'dmAddFavorites');
		$input->setAttribute('value', 'Add Checked To Favorites');
		$input_div->appendChild($input);

		$dxml->documentElement->appendChild($input_div);
		return $dxml;
	}

}
